// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © md65

//@version=6
indicator("Order Blocks FVG & BOS", overlay = true,
     max_boxes_count = 500, max_labels_count = 500, max_lines_count = 500, max_bars_back = 5000)

//#region CONSTANTS
// Core constants that control stored OB capacity and label size.
const int MAX_STORED_OBS = 50

lineStyleFromString(string value) =>
    switch value
        "Dotted" => line.style_dotted
        "Dashed" => line.style_dashed
        => line.style_solid

labelSizeFromString(string value) =>
    switch value
        "Tiny" => size.tiny
        "Small" => size.small
        "Normal" => size.normal
        "Large" => size.large
        => size.auto
//#endregion

//#region INPUTS
// Core user controls for OB behavior, volume delta, and alerts.
grpOB = "ORDER BLOCKS"
obEnabled = input.bool(true, "Enabled", group = grpOB, inline = "obEnable", tooltip = "Turns Volume Delta order-block detection and drawings on or off.")
obPivotLength = input.int(5, "Order Block Pivot", minval = 1, maxval = 50, group = grpOB, inline = "sw", tooltip = "Number of bars required on each side to confirm the pivot used by the order-block engine. Higher values create fewer, larger-structure blocks and add confirmation delay.")
bullObColor = input.color(color.new(color.teal, 55), "", group = grpOB, inline = "sw", tooltip = "Color used for order blocks.")
bearObColor = input.color(color.new(color.red, 55), "", group = grpOB, inline = "sw", tooltip = "Color used for order blocks.")
invMethod = input.string("Wick", "Invalidation", options = ["Wick", "Close"], group = grpOB, display = display.none, tooltip = "Wick invalidates when price pierces the block; Close waits for a candle close beyond it.")
showNearestX = input.int(3, "Show Nearest", minval = 1, maxval = 20, group = grpOB, display = display.none, tooltip = "Maximum active blocks shown per direction, ranked by distance from current price.")
dominantPctMin = input.float(0.0, "Minimum Dominant Volume %", minval = 0.0, maxval = 100.0, step = 1.0, group = grpOB,
 tooltip = "Only display/order alerts for blocks whose dominant side is at least this percentage. Set to 60 to match the video example.")
showHistoricZones = input.bool(true, "Show Historic Zones", group = grpOB, tooltip = "Controls whether invalidated or completed historical zones may be drawn. It does not control active zones.")
visibleExtendedZones = input.int(10, "Visible Extended Zones", minval = 0, group = grpOB,
 tooltip = "Number of bars to extend active zones to the right.")
showRetestLbl = input.bool(true, "Retest Labels", group = grpOB, tooltip = "Displays a triangle on a qualifying closed-candle retest of a displayed order block.")
retestMarkerSize = labelSizeFromString(input.string("Small", "Retest Marker Size", options = ["Tiny", "Small", "Normal", "Large", "Auto"], group = grpOB, tooltip = "Controls the size of bullish and bearish retest triangle markers."))
retestBullColor = input.color(color.new(color.teal, 0), "Bullish Retest Color", group = grpOB, inline = "retColor", tooltip = "Color used for order-block retest triangle markers.")
retestBearColor = input.color(color.new(color.red, 0), "Bearish Retest Color", group = grpOB, inline = "retColor", tooltip = "Color used for order-block retest triangle markers.")
hideInvalid = input.bool(true, "Hide Invalidated Zones", group = grpOB, inline = "tog", tooltip = "When enabled, invalidated blocks are hidden. When disabled, they can be shown if Show Historic Zones is also enabled.")

grpOBX = "ORDER BLOCK OPTIONS"
obVolumetricInfo = input.bool(true, "Volumetric Info", group = grpOBX, inline = "obx", tooltip = "Shows total volume and bullish/bearish delta percentages inside order blocks.")
obMidline = input.bool(false, "Midline", group = grpOBX, inline = "obx", tooltip = "Draws the geometric midpoint of each order block.")

grpVD = "VOLUME DELTA"
vdEnable = input.bool(true, "Enable", group = grpVD, inline = "vd", tooltip = "Enables volume-delta split fills and volume text inside displayed order blocks.")
vdBullColor = input.color(color.new(color.teal, 65), "", group = grpVD, inline = "vd", tooltip = "Color used for volume-delta split fills.")
vdBearColor = input.color(color.new(color.red, 65), "", group = grpVD, inline = "vd", tooltip = "Color used for volume-delta split fills.")
displayStyle = input.string("Vertical", "Display Style", options = ["Vertical", "Horizontal"], group = grpVD, 
 tooltip = "Horizontal: split shown top/bottom.\nVertical: split shown left/right across the zone.", display = display.none)
vdTfIn = input.timeframe("", "Volume Delta Timeframe", group = grpVD, 
 tooltip = "Lower timeframe used to estimate delta")
showTotalVol = input.bool(true, "Display Total Volume", group = grpVD, 
 tooltip = "Displays total volume (Bull+Bear) from the active delta source.", inline = "vd2") and vdEnable
showDeltaPct = input.bool(true, "Show Delta %", group = grpVD, inline = "vd2", 
 tooltip = "Shows bullish vs bearish volume split.\nIf selected TF is lower than chart TF, uses LTF data; otherwise uses chart TF.") and vdEnable
vdTextColor = input.color(color.white, "", group = grpVD, inline = "vd2", tooltip = "Text color used for volume, delta, and percentage labels.")
showPoc = input.bool(true, "Show POC", group = grpVD, inline = "poc",
 tooltip = "Draws the most-touched price level inside each displayed block.")
pocColor = input.color(color.new(color.yellow, 0), "", group = grpVD, inline = "poc", tooltip = "Color used for the point-of-control line.")
pocWidth = input.int(1, "POC Width", minval = 1, maxval = 4, group = grpVD, display = display.none, tooltip = "Line width used for the point-of-control line.")
pocStyle = lineStyleFromString(input.string("Dashed", "POC Line Type", options = ["Solid", "Dashed", "Dotted"], group = grpVD, tooltip = "Line style used for the point-of-control level inside each displayed order block."))

grpPA = "PRICE ACTION CONFLUENCE"
showFvg = input.bool(true, "Enabled", group = grpPA, inline = "fvg", tooltip = "Enables fair-value-gap detection and display.")
fvgBullColor = input.color(color.new(color.lime, 82), "", group = grpPA, inline = "fvg", tooltip = "Color used for fair-value gaps.")
fvgBearColor = input.color(color.new(color.orange, 82), "", group = grpPA, inline = "fvg", tooltip = "Color used for fair-value gaps.")
fvgDetectionMethod = input.string("Same", "Detection Method", options = ["Same", "Any"], group = grpPA, tooltip = "Same requires the three candles to agree directionally; Any accepts any three-candle gap.")
fvgInvalidationMethod = input.string("Close", "Invalidation Method", options = ["Close", "Wick"], group = grpPA, tooltip = "Close invalidates after a candle closes through the gap; Wick invalidates on a price pierce.")
fvgSensitivity = input.int(5, "Sensitivity", minval = 1, maxval = 50, group = grpPA, tooltip = "Minimum gap size is ATR divided by this value. Higher values allow smaller gaps.")
fvgVolumetricInfo = input.bool(false, "Volumetric Info", group = grpPA, inline = "fvgx", tooltip = "Adds the source volume to FVG labels.")
fvgCombineZones = input.bool(true, "Combine Zones", group = grpPA, inline = "fvgx", tooltip = "Suppresses a new same-direction FVG when it overlaps an existing active FVG.")
fvgMidline = input.bool(true, "Midline", group = grpPA, inline = "fvgx", tooltip = "Draws a dotted line through the center of each FVG.")
fvgLabels = input.bool(true, "Labels", group = grpPA, inline = "fvgl", tooltip = "Places a centered FVG label inside each active gap.")
fvgLabelColor = input.color(color.white, "", group = grpPA, inline = "fvgl", tooltip = "Text color used for FVG and IFVG labels.")
fvgMaxStored = input.int(20, "Stored FVGs", minval = 1, maxval = 100, group = grpPA, display = display.none, tooltip = "Maximum number of FVG records retained for display and confluence checks.")
showBos = input.bool(true, "Show Break of Structure", group = grpPA, inline = "bos", tooltip = "Shows the dotted swing-to-break BOS line and centered BOS label.")
bosBullColor = input.color(color.new(color.lime, 0), "", group = grpPA, inline = "bos", tooltip = "Color used for BOS lines and labels.")
bosBearColor = input.color(color.new(color.red, 0), "", group = grpPA, inline = "bos", tooltip = "Color used for BOS lines and labels.")
showConfluence = input.bool(true, "Show OB + FVG + BOS Signals", group = grpPA,
 tooltip = "Draws a LONG or SHORT label only when a new Volume Delta order block, a same-direction active FVG, and the confirmed BOS align. Unchecking it removes those labels and disables their alerts.")
confluenceWindow = input.int(20, "Confluence FVG Window", minval = 1, maxval = 100, group = grpPA, display = display.none, tooltip = "Maximum age in bars for an active same-direction FVG to qualify for an OB + FVG + BOS signal.")
confluenceBullColor = input.color(color.new(color.lime, 0), "Bullish Signal Color", group = grpPA, inline = "confStyle", tooltip = "Color used for OB + FVG + BOS signal labels.")
confluenceBearColor = input.color(color.new(color.red, 0), "Bearish Signal Color", group = grpPA, inline = "confStyle", tooltip = "Color used for OB + FVG + BOS signal labels.")
confluenceLabelSize = labelSizeFromString(input.string("Small", "Signal Label Size", options = ["Tiny", "Small", "Normal", "Large", "Auto"], group = grpPA, tooltip = "Controls the size of LONG and SHORT OB + FVG + BOS labels."))
bosLabelSize = labelSizeFromString(input.string("Tiny", "BOS Label Size", options = ["Tiny", "Small", "Normal", "Large", "Auto"], group = grpPA, tooltip = "Controls the size of the centered BOS label on each structure line."))

grpIFVG = "INVERSION FAIR VALUE GAPS"
showInversionFvg = input.bool(false, "Enabled", group = grpIFVG, inline = "ifvg", tooltip = "Displays an opposite-direction FVG after an original gap is filled.")
ifvgBullColor = input.color(color.new(color.teal, 82), "", group = grpIFVG, inline = "ifvg", tooltip = "Color used for inversion fair-value gaps.")
ifvgBearColor = input.color(color.new(color.red, 82), "", group = grpIFVG, inline = "ifvg", tooltip = "Color used for inversion fair-value gaps.")
ifvgInvalidationMethod = input.string("Wick", "Invalidation Method", options = ["Close", "Wick"], group = grpIFVG, tooltip = "Close or wick condition used to invalidate an inversion FVG.")
ifvgVolumetricInfo = input.bool(false, "Volumetric Info", group = grpIFVG, inline = "ifvgx", tooltip = "Adds source volume to inversion FVG labels.")
ifvgMidline = input.bool(false, "Midline", group = grpIFVG, inline = "ifvgx", tooltip = "Draws a dotted midpoint line through inversion FVGs.")

grpBreaker = "BREAKER BLOCKS"
breakerEnabled = input.bool(false, "Enabled", group = grpBreaker, inline = "br", tooltip = "Displays invalidated order blocks using breaker-block colors.")
breakerBullColor = input.color(color.new(color.blue, 78), "", group = grpBreaker, inline = "br", tooltip = "Color used for breaker blocks.")
breakerBearColor = input.color(color.new(color.olive, 78), "", group = grpBreaker, inline = "br", tooltip = "Color used for breaker blocks.")
breakerInvalidationMethod = input.string("Wick", "Invalidation Method", options = ["Wick", "Close"], group = grpBreaker, tooltip = "Invalidation preference for breaker presentation.")
breakerVolumetricInfo = input.bool(false, "Volumetric Info", group = grpBreaker, inline = "brx", tooltip = "Shows volume information on displayed breaker blocks.")
breakerMidline = input.bool(false, "Midline", group = grpBreaker, inline = "brx", tooltip = "Draws a midpoint line through displayed breaker blocks.")

grpTf = "TIMEFRAME OUTPUT FILTERS"
tfChart = input.bool(true, "Chart", group = grpTf, inline = "tfChart", tooltip = "Allows chart-timeframe order-block and structure output.")
tfChartSource = input.timeframe("", "", group = grpTf, inline = "tfChart", tooltip = "Optional chart timeframe source selector.")
tf1h = input.bool(false, "1 hour", group = grpTf, inline = "tf1h", tooltip = "Allows output when the active chart timeframe matches the selected one-hour source.")
tf1hSource = input.timeframe("60", "", group = grpTf, inline = "tf1h", tooltip = "Higher-timeframe source used by the one-hour selector.")
tf1w = input.bool(false, "1 week", group = grpTf, inline = "tf1w", tooltip = "Allows output when the active chart timeframe matches the selected one-week source.")
tf1wSource = input.timeframe("1W", "", group = grpTf, inline = "tf1w", tooltip = "Higher-timeframe source used by the one-week selector.")

grpMS = "MARKET STRUCTURES"
msSwingLength = input.int(14, "Swing Length", minval = 1, maxval = 100, group = grpMS, tooltip = "Number of bars on each side used to confirm market-structure pivots.")
msBullColor = input.color(color.new(color.teal, 0), "", group = grpMS, inline = "ms", tooltip = "Color used for market-structure output.")
msBearColor = input.color(color.new(color.red, 0), "", group = grpMS, inline = "ms", tooltip = "Color used for market-structure output.")
msBos = input.bool(true, "BOS", group = grpMS, inline = "msx", tooltip = "Enables the market-structure BOS line and centered label.")
msBosCandleColoring = input.bool(false, "BOS Candle Coloring", group = grpMS, tooltip = "Colors the confirmed BOS candle with the bullish or bearish market-structure color.")

grpEq = "EQUAL HIGHS & LOWS"
showEqh = input.bool(true, "EQH", group = grpEq, inline = "eq", tooltip = "Draws a dotted EQH line and centered EQH label when a newly confirmed pivot high is within 0.10 ATR of the prior confirmed pivot high.")
showEql = input.bool(true, "EQL", group = grpEq, inline = "eq", tooltip = "Draws a dotted EQL line and centered EQL label when a newly confirmed pivot low is within 0.10 ATR of the prior confirmed pivot low.")
eqLabelSize = labelSizeFromString(input.string("Tiny", "EQH / EQL Label Size", options = ["Tiny", "Small", "Normal", "Large", "Auto"], group = grpEq, tooltip = "Controls the size of the centered EQH and EQL labels drawn on their dotted equal-pivot lines."))

int swingLen = obPivotLength
bool chartTfAllowed = tfChart and (tfChartSource == "" or timeframe.period == tfChartSource)
bool timeframeAllowed = chartTfAllowed or (tf1h and timeframe.period == tf1hSource) or (tf1w and timeframe.period == tf1wSource)

grpAL = "ALERTS"
alBullOB = input.bool(true, "Bullish Order Block", group = grpAL, inline = "oba", tooltip = "Enables alerts when a bullish order block is detected.")
alBearOB = input.bool(true, "Bearish Order Block", group = grpAL, inline = "oba", tooltip = "Enables alerts when a bearish order block is detected.")
alBullRetest = input.bool(true, "Bullish OB Retest", group = grpAL, inline = "obr", tooltip = "Enables alerts when price retests a bullish order block.")
alBearRetest = input.bool(true, "Bearish OB Retest", group = grpAL, inline = "obr", tooltip = "Enables alerts when price retests a bearish order block.")
//#endregion

//#region TYPES
type ObRec
    int leftIndex
    int leftTime
    int createdIndex
    int createdTime
    float top
    float bottom
    bool isBull
    bool active
    bool retested
    int retestIndex
    int retestTime
    int invalidIndex
    int invalidTime
    float bullVol
    float bearVol
    float totalVol
    float bullPct
    float bearPct
    float poc
    bool hasDelta

type RetestRec
    int barIndex
    bool isBull
    int obLeftIndex
    int obCreatedIndex

type FvgRec
    int leftIndex
    float top
    float bottom
    bool isBull
    bool active
    bool isInversion

type BosRec
    int startIndex
    int endIndex
    int startTime
    int endTime
    float level
    bool isBull

type EqRec
    int startIndex
    int endIndex
    int startTime
    int endTime
    float level
    bool isHigh
//#endregion

//#region GENERIC HELPERS
// Small utilities: clearing drawings, geometry helpers, nearest-OB picking.
method clearAll(array<box> bx, array<label> lb) =>
    if bx.size() > 0
        for i = 0 to bx.size() - 1
            bx.get(i).delete()
    bx.clear()
    if lb.size() > 0
        for i = 0 to lb.size() - 1
            lb.get(i).delete()
    lb.clear()

// Returns true if a candle's high/low intersects the OB zone.
touchesZone(float zTop, float zBot, float cHigh, float cLow) =>
    cHigh >= zBot and cLow <= zTop

// Distance from price to zone in price units, used to rank nearest zones.
zoneDistance(float px, float zTop, float zBot) =>
    px > zTop ? px - zTop : px < zBot ? zBot - px : 0.0

// Inserts a distance+index pair into a sorted list, capped at kMax.
method insertBest(array<float> dists, array<int> idxs, float dist, int idx, int kMax) =>
    if dists.size() == 0
        dists.push(dist)
        idxs.push(idx)
    else
        int pos = dists.size()
        if dists.size() > 0
            for j = 0 to dists.size() - 1
                if dist < dists.get(j)
                    pos := j
                    break
        dists.insert(pos, dist)
        idxs.insert(pos, idx)
    while dists.size() > kMax
        dists.pop()
        idxs.pop()

// Picks the k nearest bull/bear OBs to current price (optionally including invalid).
pickNearest(array<ObRec> store, bool wantBull, int kMax, bool includeInvalid) =>
    array<float> dists = array.new_float()
    array<int> idxs = array.new_int()
    if store.size() > 0
        for i = 0 to store.size() - 1
            ObRec ob = store.get(i)
            bool dominantOk = wantBull ? ob.bullPct >= dominantPctMin : ob.bearPct >= dominantPctMin
            if ob.isBull == wantBull and dominantOk
                bool ok = includeInvalid ? true : ob.active
                if ok
                    float dist = zoneDistance(close, ob.top, ob.bottom)
                    dists.insertBest(idxs, dist, i, kMax)
    idxs

formatVol(float v) =>
    str.tostring(v, format.volume)
//#endregion

//#region VOLUME ENGINE (CHART OR LOWER TF)
// Per-bar total / bull / bear volume
tfSec(string tf) =>
    timeframe.in_seconds(tf)

int chartSec = tfSec(timeframe.period)
int srcSec = tfSec(vdTfIn)
bool useLtf = not na(chartSec) and not na(srcSec) and srcSec < chartSec

getBarVols() =>
    float tot = na
    float bull = na
    float bear = na
    if not useLtf or vdTfIn == ""
        float v = volume
        bool up = close > open
        bool dn = close < open
        tot := v
        bull := up ? v : 0.0
        bear := dn ? v : 0.0
    else
        array<float> oArr = request.security_lower_tf(syminfo.tickerid, vdTfIn, open)
        array<float> cArr = request.security_lower_tf(syminfo.tickerid, vdTfIn, close)
        array<float> vArr = request.security_lower_tf(syminfo.tickerid, vdTfIn, volume)
        float tSum = 0.0
        float bSum = 0.0
        float sSum = 0.0
        int n = array.size(vArr)
        if n > 0
            for i = 0 to n - 1
                float v2 = array.get(vArr, i)
                float o2 = array.get(oArr, i)
                float c2 = array.get(cArr, i)
                tSum += v2
                if c2 > o2
                    bSum += v2
                else if c2 < o2
                    sSum += v2
        tot := tSum
        bull := bSum
        bear := sSum
    [tot, bull, bear]

[barTotVol, barBullVol, barBearVol] = getBarVols()
//#endregion

//#region POC ENGINE (MOST-TOUCHED PRICE + VOLUME)
// Finds a POC between swing and BOS, then aggregates volume.
findMostTouchedPrice(int fromIdx, int toIdx, int nBins) =>
    int span = toIdx - fromIdx
    if span <= 0 or na(fromIdx) or na(toIdx)
        [na, 0]
    else
        float minP = 1e10
        float maxP = -1e10
        for idx = fromIdx to toIdx
            int rel = bar_index - idx
            if rel >= 0
                float lo = low[rel]
                float hi = high[rel]
                if not na(lo) and not na(hi)
                    if lo < minP
                        minP := lo
                    if hi > maxP
                        maxP := hi
        if not (minP < maxP)
            [na, 0]
        else
            float step = (maxP - minP) / nBins
            step := step <= 0 ? syminfo.mintick : step
            array<float> diffCnt = array.new_float(nBins, 0.0)
            for idx = fromIdx to toIdx
                int rel2 = bar_index - idx
                if rel2 >= 0
                    float lo2 = low[rel2]
                    float hi2 = high[rel2]
                    if not na(lo2) and not na(hi2)
                        int sBin = int(math.floor((lo2 - minP) / step))
                        int eBin = int(math.floor((hi2 - minP) / step))
                        sBin := sBin < 0 ? 0 : sBin > nBins - 1 ? nBins - 1 : sBin
                        eBin := eBin < 0 ? 0 : eBin > nBins - 1 ? nBins - 1 : eBin
                        float cStart = diffCnt.get(sBin)
                        diffCnt.set(sBin, cStart + 1.0)
                        if eBin + 1 < nBins
                            float cEnd = diffCnt.get(eBin + 1)
                            diffCnt.set(eBin + 1, cEnd - 1.0)
            int   bestBin = 0
            float bestCnt = 0.0
            float runCnt = 0.0
            for i = 0 to nBins - 1
                runCnt += diffCnt.get(i)
                if runCnt > bestCnt
                    bestCnt := runCnt
                    bestBin := i
            float poc = minP + (bestBin + 0.5) * step
            [poc, int(bestCnt)]

// Aggregates total / bull / bear volume at a single price level (POC) for a range.
volumeAtPrice(int fromIdx, int toIdx, float poc) =>
    int   touches = 0
    float totVol = 0.0
    float bullVol = 0.0
    float bearVol = 0.0
    int span = toIdx - fromIdx
    if not na(poc) and span >= 0
        for step = 0 to span
            int idx = toIdx - step
            int rel = bar_index - idx
            if rel >= 0
                float lo = low[rel]
                float hi = high[rel]
                if not na(lo) and not na(hi) and lo <= poc and hi >= poc
                    touches += 1
                    float vTot = barTotVol[rel]
                    float vBull = barBullVol[rel]
                    float vBear = barBearVol[rel]
                    if not na(vTot)
                        totVol += vTot
                    if not na(vBull)
                        bullVol += vBull
                    if not na(vBear)
                        bearVol += vBear
    [touches, totVol, bullVol, bearVol]

// Wrapper: find POC first, then compute volume at that POC for the BOS range.
calcMostTouchedPriceVol(int fromIdx, int toIdx, int nBins) =>
    [poc, _approxTouches] = findMostTouchedPrice(fromIdx, toIdx, nBins)
    [touches, totVol, bullVol, bearVol] = volumeAtPrice(fromIdx, toIdx, poc)
    [poc, touches, totVol, bullVol, bearVol]
//#endregion

//#region ORDER BLOCK ENGINE (POC-BASED)
// Detects swing highs/lows, confirms BOS, anchors OB at first POC touch.
var array<ObRec> obs = array.new<ObRec>()
var array<RetestRec> obRetests = array.new<RetestRec>()

var int lastBullRetestBar = na
var int lastBearRetestBar = na

var float shPrice = na
var int   shIdx = na
var int   shTime = na
var float slPrice = na
var int   slIdx = na
var int   slTime = na

bool evNewBullOB = false
bool evNewBearOB = false
bool evBullRetest = false
bool evBearRetest = false

float ph = ta.pivothigh(high, swingLen, swingLen)
float pl = ta.pivotlow(low, swingLen, swingLen)

if not na(ph)
    shPrice := ph
    shIdx := bar_index - swingLen
    shTime := time[swingLen]

if not na(pl)
    slPrice := pl
    slIdx := bar_index - swingLen
    slTime := time[swingLen]

bool bosBearNow = not na(slPrice) and bar_index > slIdx and close < slPrice and close[1] >= slPrice
bool bosBullNow = not na(shPrice) and bar_index > shIdx and close > shPrice and close[1] <= shPrice

bool bosBear = bosBearNow[1]
bool bosBull = bosBullNow[1]

var array<BosRec> bosRecords = array.new<BosRec>()
if bosBull and not na(shIdx[1]) and not na(shPrice[1])
    bosRecords.unshift(BosRec.new(shIdx[1], bar_index, shTime[1], time, shPrice[1], true))
if bosBear and not na(slIdx[1]) and not na(slPrice[1])
    bosRecords.unshift(BosRec.new(slIdx[1], bar_index, slTime[1], time, slPrice[1], false))
while bosRecords.size() > 100
    bosRecords.pop()

// Precompute BOS ranges and POC stats in global scope
int bosIdxBear = bar_index - 1
int fromIdxBear = slIdx[1]
int toIdxBear = bosIdxBear
[pocB, touchesB, totVolB, bullVolB, bearVolB] = calcMostTouchedPriceVol(fromIdxBear, toIdxBear, 40)

int bosIdxBull = bar_index - 1
int fromIdxBull = shIdx[1]
int toIdxBull = bosIdxBull
[pocH, touchesH, totVolH, bullVolH, bearVolH] = calcMostTouchedPriceVol(fromIdxBull, toIdxBull, 40)

// Keeps OB array trimmed to recent history and limits max stored OBs.
pruneObs() =>
    int minLeft = math.max(0, bar_index - 4999)
    if obs.size() > 0
        for i = obs.size() - 1 to 0
            ObRec ob = obs.get(i)
            if ob.leftIndex < minLeft
                obs.remove(i)
    while obs.size() > MAX_STORED_OBS
        bool removed = false
        if obs.size() > 0
            for j = obs.size() - 1 to 0
                ObRec ob2 = obs.get(j)
                if not ob2.active
                    obs.remove(j)
                    removed := true
                    break
        if not removed and obs.size() > 0
            obs.pop()

// Creates and seeds an OB record using a POC-anchored candle and BOS volumes.
addObFromPoc(int baseIdx, float top, float bottom, bool isBull, float totSeed, float bullSeed, float bearSeed, float pocSeed, int createdIdx) =>
    int offLeft = bar_index - baseIdx
    int offCreated = bar_index - createdIdx
    int leftTime = time[offLeft]
    int createdTm = time[offCreated]
    float tot = totSeed
    float bVol = bullSeed
    float sVol = bearSeed
    bool hasDelta = tot > 0.0
    float bullPct = hasDelta ? math.round((bVol / tot) * 100.0) : 50.0
    float bearPct = hasDelta ? 100.0 - bullPct : 50.0
    obs.unshift(ObRec.new(baseIdx, leftTime, createdIdx, createdTm, top, bottom, isBull,
     true, false, na, na, na, na, bVol, sVol, tot, bullPct, bearPct, pocSeed, hasDelta))
    pruneObs()

// Returns true if a proposed zone overlaps any active OB 
obOverlapsActive(float zoneTop, float zoneBottom) =>
    float zTop = math.max(zoneTop, zoneBottom)
    float zBot = math.min(zoneTop, zoneBottom)
    bool overlaps = false
    if obs.size() > 0
        for i = 0 to obs.size() - 1
            ObRec ob = obs.get(i)
            if ob.active
                float oTop = math.max(ob.top, ob.bottom)
                float oBot = math.min(ob.top, ob.bottom)
                bool rangeOverlap = zTop >= oBot and zBot <= oTop
                if rangeOverlap
                    overlaps := true
                    break
    overlaps

// Returns true if there is a price gap between anchorIdx and bosIdx.
hasGapBetween(int anchorIdx, int bosIdx, bool isBull) =>
    bool gap = false
    int fromIdx = math.min(anchorIdx, bosIdx)
    int toIdx = math.max(anchorIdx, bosIdx)
    if toIdx - fromIdx >= 1
        for absIdx = fromIdx + 1 to toIdx
            int relNow = bar_index - absIdx
            int relPrev = relNow + 1  
            if relNow >= 0 and relPrev >= 0
                float hiPrev = high[relPrev]
                float loPrev = low[relPrev]
                float hiNow = high[relNow]
                float loNow = low[relNow]
                if not na(hiPrev) and not na(loPrev) and not na(hiNow) and not na(loNow)
                    if isBull
                        if loNow > hiPrev
                            gap := true
                            break
                    else
                        if hiNow < loPrev
                            gap := true
                            break
    gap

// Bearish BOS → Bearish OB
if obEnabled and timeframeAllowed and bosBear
    int bosIdx = bosIdxBear
    int fromIdx = fromIdxBear
    int toIdx = toIdxBear
    if not na(pocB) and touchesB > 0 and not na(fromIdx) and not na(toIdx)
        int spanB = toIdx - fromIdx
        int bestIdx = na
        float runMaxHigh = na
        if spanB >= 0
            for step = 0 to spanB
                int idx = toIdx - step
                int rel = bar_index - idx
                if rel >= 0
                    float lo = low[rel]
                    float hi = high[rel]
                    if not na(lo) and not na(hi)
                        runMaxHigh := na(runMaxHigh) ? hi : math.max(runMaxHigh, hi)
                        bool touches = lo <= pocB and hi >= pocB
                        if touches and hi == runMaxHigh
                            bestIdx := idx
        bool gapLeg = not na(bestIdx) ? hasGapBetween(bestIdx, bosIdx, false) : false
        if not na(bestIdx) and not gapLeg
            int relBest = bar_index - bestIdx
            float top = high[relBest]
            float bottom = low[relBest]
            if not obOverlapsActive(top, bottom)
                addObFromPoc(bestIdx, top, bottom, false, totVolB, bullVolB, bearVolB, pocB, bosIdx)
                evNewBearOB := true
    slPrice := na
    slIdx := na

// Bullish BOS → Bullish OB
if obEnabled and timeframeAllowed and bosBull
    int bosIdx2 = bosIdxBull
    int fromIdx2 = fromIdxBull
    int toIdx2 = toIdxBull
    if not na(pocH) and touchesH > 0 and not na(fromIdx2) and not na(toIdx2)
        int spanH = toIdx2 - fromIdx2
        int bestIdx2 = na
        float runMinLow = na
        if spanH >= 0
            for step = 0 to spanH
                int idx2 = toIdx2 - step
                int rel2 = bar_index - idx2
                if rel2 >= 0
                    float lo2 = low[rel2]
                    float hi2 = high[rel2]
                    if not na(lo2) and not na(hi2)
                        runMinLow := na(runMinLow) ? lo2 : math.min(runMinLow, lo2)
                        bool touches = lo2 <= pocH and hi2 >= pocH
                        if touches and lo2 == runMinLow
                            bestIdx2 := idx2
        bool gapLeg2 = not na(bestIdx2) ? hasGapBetween(bestIdx2, bosIdx2, true) : false
        if not na(bestIdx2) and not gapLeg2
            int relBest2 = bar_index - bestIdx2
            float top2 = high[relBest2]
            float bottom2 = low[relBest2]
            if not obOverlapsActive(top2, bottom2)
                addObFromPoc(bestIdx2, top2, bottom2, true, totVolH, bullVolH, bearVolH, pocH, bosIdx2)
                evNewBullOB := true
    shPrice := na
    shIdx := na

// Invalidation and retest detection for existing OBs.
if obs.size() > 0
    for i = 0 to obs.size() - 1
        ObRec ob = obs.get(i)
        if ob.active
            bool invalid = false
            int invIdx = na
            int invTime = na

            if ob.isBull
                if invMethod == "Wick"
                    invalid := low < ob.bottom
                    invIdx := bar_index
                    invTime := time
                else
                    if bar_index > 0
                        invalid := close[1] < ob.bottom
                        invIdx := bar_index - 1
                        invTime := time[1]
            else
                if invMethod == "Wick"
                    invalid := high > ob.top
                    invIdx := bar_index
                    invTime := time
                else
                    if bar_index > 0
                        invalid := close[1] > ob.top
                        invIdx := bar_index - 1
                        invTime := time[1]

            if invalid
                ob.active := false
                ob.invalidIndex := invIdx
                ob.invalidTime := invTime

            bool retestPrev = false
            int retestBar = na
            int retestTm = na

            if bar_index > 0
                if ob.isBull
                    bool opensAbovePrev = open[1] > ob.top
                    bool closesAbovePrev = close[1] > ob.top
                    bool wickTouchesPrev = low[1] <= ob.top and low[1] >= ob.bottom
                    retestPrev := opensAbovePrev and closesAbovePrev and wickTouchesPrev
                else
                    bool opensBelowPrev = open[1] < ob.bottom
                    bool closesBelowPrev = close[1] < ob.bottom
                    bool wickTouchesPrev = high[1] >= ob.bottom and high[1] <= ob.top
                    retestPrev := opensBelowPrev and closesBelowPrev and wickTouchesPrev

                if retestPrev
                    retestBar := bar_index - 1  
                    retestTm := time[1]

            if retestPrev and not na(retestBar) and retestBar > ob.createdIndex
                ob.retested := true
                ob.retestIndex := retestBar
                ob.retestTime := retestTm

                int lastSideBar = ob.isBull ? lastBullRetestBar : lastBearRetestBar
                bool canLog = na(lastSideBar) or retestBar - lastSideBar >= 4 

                if canLog
                    obRetests.unshift(RetestRec.new(retestBar, ob.isBull, ob.leftIndex, ob.createdIndex))

                    if ob.isBull
                        evBullRetest := true
                        lastBullRetestBar := retestBar
                    else
                        evBearRetest := true
                        lastBearRetestBar := retestBar

        obs.set(i, ob)
//#endregion

//#region FVG AND CONFLUENCE ENGINE
var array<FvgRec> fvgs = array.new<FvgRec>()

bool sameBullFvg = close > open and close[1] > open[1] and close[2] > open[2]
bool sameBearFvg = close < open and close[1] < open[1] and close[2] < open[2]
float fvgMinGap = ta.atr(14) / fvgSensitivity
bool newBullFvg = bar_index >= 2 and low > high[2] and low - high[2] >= fvgMinGap and (fvgDetectionMethod == "Any" or sameBullFvg)
bool newBearFvg = bar_index >= 2 and high < low[2] and low[2] - high >= fvgMinGap and (fvgDetectionMethod == "Any" or sameBearFvg)

fvgOverlaps(bool wantBull, float newTop, float newBottom) =>
    bool overlap = false
    if fvgs.size() > 0
        for i = 0 to fvgs.size() - 1
            FvgRec oldFvg = fvgs.get(i)
            if oldFvg.active and oldFvg.isBull == wantBull and newTop >= oldFvg.bottom and newBottom <= oldFvg.top
                overlap := true
                break
    overlap

if newBullFvg and (not fvgCombineZones or not fvgOverlaps(true, low, high[2]))
    fvgs.unshift(FvgRec.new(bar_index - 2, low, high[2], true, true, false))
if newBearFvg and (not fvgCombineZones or not fvgOverlaps(false, low[2], high))
    fvgs.unshift(FvgRec.new(bar_index - 2, low[2], high, false, true, false))

if fvgs.size() > 0
    for i = fvgs.size() - 1 to 0
        FvgRec f = fvgs.get(i)
        if f.active
            bool filled = f.isInversion ? (ifvgInvalidationMethod == "Close" ? (f.isBull ? close <= f.bottom : close >= f.top) : (f.isBull ? low <= f.bottom : high >= f.top)) : (fvgInvalidationMethod == "Close" ? (f.isBull ? close <= f.bottom : close >= f.top) : (f.isBull ? low <= f.bottom : high >= f.top))
            if filled
                f.active := false
                if showInversionFvg and not f.isInversion
                    fvgs.unshift(FvgRec.new(f.leftIndex, f.top, f.bottom, not f.isBull, true, true))
            fvgs.set(i, f)
    while fvgs.size() > fvgMaxStored
        fvgs.pop()

hasRecentActiveFvg(bool wantBull) =>
    bool found = false
    if fvgs.size() > 0
        for i = 0 to fvgs.size() - 1
            FvgRec f = fvgs.get(i)
            if f.active and f.isBull == wantBull and bar_index - f.leftIndex <= confluenceWindow
                found := true
                break
    found

bool bullConfluence = showConfluence and evNewBullOB and bosBull and hasRecentActiveFvg(true)
bool bearConfluence = showConfluence and evNewBearOB and bosBear and hasRecentActiveFvg(false)
//#endregion

//#region MARKET STRUCTURE ENGINE
float msPivotHigh = ta.pivothigh(high, msSwingLength, msSwingLength)
float msPivotLow = ta.pivotlow(low, msSwingLength, msSwingLength)
var float msLastHigh = na
var float msLastLow = na
if not na(msPivotHigh)
    msLastHigh := msPivotHigh
if not na(msPivotLow)
    msLastLow := msPivotLow

bool msBullBreak = not na(msLastHigh) and close > msLastHigh and close[1] <= msLastHigh
bool msBearBreak = not na(msLastLow) and close < msLastLow and close[1] >= msLastLow
bool msBullBos = msBullBreak
bool msBearBos = msBearBreak

var float eqLastHigh = na
var float eqLastLow = na
var int eqLastHighIndex = na
var int eqLastLowIndex = na
var int eqLastHighTime = na
var int eqLastLowTime = na
var array<EqRec> eqRecords = array.new<EqRec>()
float eqTolerance = ta.atr(14) * 0.10
bool eqhEvent = showEqh and not na(msPivotHigh) and not na(eqLastHigh) and math.abs(msPivotHigh - eqLastHigh) <= eqTolerance
bool eqlEvent = showEql and not na(msPivotLow) and not na(eqLastLow) and math.abs(msPivotLow - eqLastLow) <= eqTolerance
if eqhEvent and not na(eqLastHighIndex)
    eqRecords.unshift(EqRec.new(eqLastHighIndex, bar_index, eqLastHighTime, time, msPivotHigh, true))
if eqlEvent and not na(eqLastLowIndex)
    eqRecords.unshift(EqRec.new(eqLastLowIndex, bar_index, eqLastLowTime, time, msPivotLow, false))
while eqRecords.size() > 100
    eqRecords.pop()
if not na(msPivotHigh)
    eqLastHigh := msPivotHigh
    eqLastHighIndex := bar_index - msSwingLength
    eqLastHighTime := time[msSwingLength]
if not na(msPivotLow)
    eqLastLow := msPivotLow
    eqLastLowIndex := bar_index - msSwingLength
    eqLastLowTime := time[msSwingLength]
//#endregion

//#region DRAW ENGINE (ZONES + VOLUME)
// Handles all boxes, labels, and volume-delta fills for OBs.
bool showVD = vdEnable
bool isVert = displayStyle == "Vertical"
bool isHorz = not isVert

var array<box> allBoxes = array.new<box>()
var array<label> allLabels = array.new<label>()
var array<line> allLines = array.new<line>()

method pushBox(array<box> store, box b) =>
    store.unshift(b)

method pushLabel(array<label> store, label l) =>
    store.unshift(l)

// Chooses base colors for OB zones depending on vdEnable and bull/bear type.
obColors(ObRec ob) =>
    color bullCol = vdEnable ? vdBullColor : bullObColor
    color bearCol = vdEnable ? vdBearColor : bearObColor
    if not ob.active and breakerEnabled
        bullCol := breakerBullColor
        bearCol := breakerBearColor
    color baseCol = ob.isBull ? bullCol : bearCol
    color faded = ob.active ? baseCol : color.new(baseCol, 85)
    [baseCol, faded]

// Computes right-most bar index for drawing an OB, considering extension and invalidation.
obRightIndex(ObRec ob) =>
    int activeRight = bar_index + visibleExtendedZones
    if ob.active
        activeRight
    else
        na(ob.invalidIndex) ? activeRight : ob.invalidIndex

// Draws the main OB zone box on the chart.
drawObZoneBox(ObRec ob, color faded) =>
    int xR = obRightIndex(ob)
    int xL = ob.leftIndex
    int minBar = bar_index - 4999
    if xL < minBar
        xL := minBar
    xR := math.max(xR, xL)
    box bx = box.new(left = xL, right = xR, top = ob.top, bottom = ob.bottom, xloc = xloc.bar_index, bgcolor = faded, border_color = na, border_width = 0)
    allBoxes.pushBox(bx)

// Draws a retest marker (triangle) when price revisits an OB.
drawRetestLabels(array<int> bullAct, array<int> bearAct, array<int> bullInv, array<int> bearInv) =>
    int ret = 0
    if showRetestLbl and obRetests.size() > 0
        int minBar = bar_index - 4999

        int lastBullLbl = na
        int lastBearLbl = na

        for i = obRetests.size() - 1 to 0
            RetestRec r = obRetests.get(i)

            if r.barIndex < minBar or r.barIndex > bar_index
                RetestRec _trash = obRetests.remove(i)
            else
                bool hasDisplayedParent = false

                if r.isBull
                    if bullAct.size() > 0
                        for j = 0 to bullAct.size() - 1
                            ObRec ob = obs.get(bullAct.get(j))
                            if ob.leftIndex == r.obLeftIndex and ob.createdIndex == r.obCreatedIndex
                                hasDisplayedParent := true
                                break

                    if not hasDisplayedParent and not hideInvalid and bullInv.size() > 0
                        for j = 0 to bullInv.size() - 1
                            ObRec ob = obs.get(bullInv.get(j))
                            if ob.leftIndex == r.obLeftIndex and ob.createdIndex == r.obCreatedIndex
                                hasDisplayedParent := true
                                break
                else
                    if bearAct.size() > 0
                        for j = 0 to bearAct.size() - 1
                            ObRec ob = obs.get(bearAct.get(j))
                            if ob.leftIndex == r.obLeftIndex and ob.createdIndex == r.obCreatedIndex
                                hasDisplayedParent := true
                                break

                    if not hasDisplayedParent and not hideInvalid and bearInv.size() > 0
                        for j = 0 to bearInv.size() - 1
                            ObRec ob = obs.get(bearInv.get(j))
                            if ob.leftIndex == r.obLeftIndex and ob.createdIndex == r.obCreatedIndex
                                hasDisplayedParent := true
                                break

                if not hasDisplayedParent
                    continue

                int age = bar_index - r.barIndex
                if age >= 0 and age <= 4999
                    if r.isBull
                        if not na(lastBullLbl) and r.barIndex - lastBullLbl < 3
                            continue
                        lastBullLbl := r.barIndex
                    else
                        if not na(lastBearLbl) and r.barIndex - lastBearLbl < 3
                            continue
                        lastBearLbl := r.barIndex

                    float yPrice = close[age]
                    color baseCol = r.isBull ? retestBullColor : retestBearColor

                    label newLbl = label.new(r.barIndex, yPrice, "", xloc = xloc.bar_index, yloc = r.isBull ? yloc.belowbar : yloc.abovebar,
                         style = r.isBull ? label.style_triangleup : label.style_triangledown,
                         color = baseCol, textcolor = baseCol, size = retestMarkerSize, force_overlay = true)

                    allLabels.pushLabel(newLbl)
    ret

// Returns geometry used for volume overlay.
obGeom(ObRec ob) =>
    int rawL = ob.leftIndex
    int rawR = obRightIndex(ob)
    int minBar = bar_index - 4999
    int xL = math.max(rawL, minBar)
    int xR = math.max(rawR, xL)
    float yT = ob.top
    float yB = ob.bottom
    float h = yT - yB
    [xL, xR, yT, yB, h]

// Converts bull/bear percentages into display strings (if enabled).
deltaTexts(float bullPct, float bearPct) =>
    string bullTxt = showDeltaPct ? str.tostring(bullPct) + "%" : ""
    string bearTxt = showDeltaPct ? str.tostring(bearPct) + "%" : ""
    [bullTxt, bearTxt]

// Draws a text label with total volume on the right-bottom corner of the OB.
drawTotalVolLabel(int xL, int xR, float yT, float yB, float h, float total) =>
    if showTotalVol and obVolumetricInfo
        int cx = xR - 1
        if cx < xL
            cx := xL
        float cy = yB + h * 0.25
        string txt = formatVol(total)
        label lb = label.new(cx, cy, txt, xloc = xloc.bar_index, style = label.style_label_right, 
         textcolor = vdTextColor, color = #ffffff00, size = size.small, force_overlay = true)
        allLabels.pushLabel(lb)

// Fills the OB with bull/bear split either vertically or horizontally.
drawDeltaFills(int xL, int xR, float yT, float yB, float h, float bullPct, string bullTxt, string bearTxt) =>
    color bullCol = vdEnable ? vdBullColor : bullObColor
    color bearCol = vdEnable ? vdBearColor : bearObColor
    if isVert
        int splitX = xL + int((xR - xL) * (bullPct / 100.0))
        splitX := xR - xL >= 2 ? math.max(xL + 1, math.min(xR - 1, splitX)) : xL
        box bBull = box.new(xL, yT, splitX, yB, xloc = xloc.bar_index, bgcolor = bullCol, border_width = 0, text = bullTxt, text_color = vdTextColor, text_size = size.small)
        box bBear = box.new(splitX, yT, xR, yB, xloc = xloc.bar_index, bgcolor = bearCol, border_width = 0, text = bearTxt, text_color = vdTextColor, text_size = size.small)
        allBoxes.pushBox(bBull)
        allBoxes.pushBox(bBear)
    else
        float midY = yB + h * (bullPct / 100.0)
        box bBull = box.new(xL, midY, xR, yB, xloc = xloc.bar_index, bgcolor = bullCol, border_width = 0, text = bullTxt, text_color = vdTextColor, text_size = size.small)
        box bBear = box.new(xL, yT, xR, midY, xloc = xloc.bar_index, bgcolor = bearCol, border_width = 0, text = bearTxt, text_color = vdTextColor, text_size = size.small)
        allBoxes.pushBox(bBull)
        allBoxes.pushBox(bBear)

// Draws a full OB: zone, retest label, and volume fill.
drawZoneAndVolume(ObRec ob) =>
    [baseCol, faded] = obColors(ob)
    bool showBlockVolume = ob.active ? obVolumetricInfo : breakerVolumetricInfo
    bool showDeltaZone = obEnabled and timeframeAllowed and showVD and showBlockVolume and ob.hasDelta
    // Volume-delta fills already provide the zone colors; draw the base OB box
    // only when the split fill is unavailable, avoiding a second color overlay.
    if not showDeltaZone
        drawObZoneBox(ob, faded)
    if obEnabled and timeframeAllowed and showPoc and ob.hasDelta and not na(ob.poc)
        int pocLeft = math.max(ob.leftIndex, bar_index - 4999)
        int pocRight = obRightIndex(ob)
        line pocLine = line.new(pocLeft, ob.poc, pocRight, ob.poc, xloc = xloc.bar_index,
             color = pocColor, width = pocWidth, style = pocStyle)
        allLines.unshift(pocLine)
    if obEnabled and timeframeAllowed and (obMidline or (not ob.active and breakerMidline))
        int midLeft = math.max(ob.leftIndex, bar_index - 4999)
        int midRight = obRightIndex(ob)
        float midPrice = math.avg(ob.top, ob.bottom)
        line obMidLine = line.new(midLeft, midPrice, midRight, midPrice, xloc = xloc.bar_index,
             color = baseCol, width = 1, style = line.style_dotted)
        allLines.unshift(obMidLine)
    if showDeltaZone
        [xL, xR, yT, yB, h] = obGeom(ob)
        [bullTxt, bearTxt] = deltaTexts(ob.bullPct, ob.bearPct)
        drawTotalVolLabel(xL, xR, yT, yB, h, ob.totalVol)
        drawDeltaFills(xL, xR, yT, yB, h, ob.bullPct, bullTxt, bearTxt)
    true

// Clear all previous drawings each bar before re-rendering current selection.
allBoxes.clearAll(allLabels)
if allLines.size() > 0
    for i = 0 to allLines.size() - 1
        allLines.get(i).delete()
allLines.clear()

if showBos and bosRecords.size() > 0
    for i = 0 to bosRecords.size() - 1
        BosRec b = bosRecords.get(i)
        int bosRight = math.max(b.endIndex, b.startIndex)
        color bosColor = b.isBull ? bosBullColor : bosBearColor
        line bosLine = line.new(b.startTime, b.level, b.endTime, b.level, xloc = xloc.bar_time,
             color = bosColor, style = line.style_dotted, width = 1)
        allLines.unshift(bosLine)
        int bosLabelX = int(math.avg(b.startTime, b.endTime))
        label bosLabel = label.new(bosLabelX, b.level, "BOS", xloc = xloc.bar_time, yloc = yloc.price,
             style = label.style_none, color = #00000000, textcolor = bosColor, size = bosLabelSize, force_overlay = true)
        allLabels.unshift(bosLabel)

if eqRecords.size() > 0
    for i = 0 to eqRecords.size() - 1
        EqRec e = eqRecords.get(i)
        bool showEqRecord = e.isHigh ? showEqh : showEql
        if showEqRecord
            color eqColor = e.isHigh ? msBearColor : msBullColor
            line eqLine = line.new(e.startTime, e.level, e.endTime, e.level, xloc = xloc.bar_time,
                 color = eqColor, style = line.style_dotted, width = 1)
            allLines.unshift(eqLine)
            int eqLabelX = int(math.avg(e.startTime, e.endTime))
            label eqLabel = label.new(eqLabelX, e.level, e.isHigh ? "EQH" : "EQL", xloc = xloc.bar_time,
                 yloc = yloc.price, style = label.style_none, color = #00000000, textcolor = eqColor,
                  size = eqLabelSize, force_overlay = true)
            allLabels.unshift(eqLabel)

if (showFvg or showInversionFvg) and fvgs.size() > 0
    for i = 0 to fvgs.size() - 1
        FvgRec f = fvgs.get(i)
        bool showThisFvg = f.active and (f.isInversion ? showInversionFvg : showFvg)
        if showThisFvg
            color fvgColor = f.isInversion ? (f.isBull ? ifvgBullColor : ifvgBearColor) : (f.isBull ? fvgBullColor : fvgBearColor)
            int fvgLeft = math.max(f.leftIndex, bar_index - 4999)
            box fvgBox = box.new(left = fvgLeft, right = bar_index + 1, top = f.top, bottom = f.bottom,
                 xloc = xloc.bar_index, bgcolor = fvgColor, border_color = na, border_width = 0)
            allBoxes.unshift(fvgBox)
            bool showFvgMidline = f.isInversion ? ifvgMidline : fvgMidline
            if showFvgMidline
                line fvgLine = line.new(fvgLeft, math.avg(f.top, f.bottom), bar_index + 1, math.avg(f.top, f.bottom),
                     xloc = xloc.bar_index, color = color.new(fvgColor, 20), style = line.style_dotted)
                allLines.unshift(fvgLine)
            bool showFvgText = f.isInversion ? ifvgVolumetricInfo : (fvgLabels or fvgVolumetricInfo)
            if showFvgText
                int fvgAge = bar_index - f.leftIndex
                string fvgText = f.isInversion ? "IFVG" : "FVG"
                bool showFvgVolume = f.isInversion ? ifvgVolumetricInfo : fvgVolumetricInfo
                if showFvgVolume and fvgAge >= 0 and fvgAge <= 4999
                    fvgText += " " + str.tostring(volume[fvgAge], format.volume)
                int fvgLabelX = fvgLeft + int(math.max(bar_index + 1 - fvgLeft, 1) / 2)
                label fvgLabel = label.new(fvgLabelX, math.avg(f.top, f.bottom), fvgText, xloc = xloc.bar_index,
                     style = label.style_none, color = #00000000, textcolor = fvgLabelColor, size = size.tiny, force_overlay = true)
                allLabels.unshift(fvgLabel)

barcolor(msBos and msBosCandleColoring ? msBullBos ? msBullColor : msBearBos ? msBearColor : na : na)

if bullConfluence
    label bullSignal = label.new(bar_index, low, "LONG\nOB + FVG + BOS", xloc = xloc.bar_index, yloc = yloc.belowbar,
         style = label.style_label_up, color = confluenceBullColor, textcolor = color.white, size = confluenceLabelSize, force_overlay = true)
    allLabels.unshift(bullSignal)
if bearConfluence
    label bearSignal = label.new(bar_index, high, "SHORT\nOB + FVG + BOS", xloc = xloc.bar_index, yloc = yloc.abovebar,
         style = label.style_label_down, color = confluenceBearColor, textcolor = color.white, size = confluenceLabelSize, force_overlay = true)
    allLabels.unshift(bearSignal)

// Pick active and invalid OBs based on hideInvalid + showNearestX.
array<int> bullActive = array.new_int()
array<int> bearActive = array.new_int()
array<int> bullInvalid = array.new_int()
array<int> bearInvalid = array.new_int()

if (hideInvalid and not breakerEnabled) or not showHistoricZones or not obEnabled or not timeframeAllowed
    bullActive := pickNearest(obs, true, showNearestX, false)
    bearActive := pickNearest(obs, false, showNearestX, false)
else
    bullActive := pickNearest(obs, true, showNearestX, false)
    bearActive := pickNearest(obs, false, showNearestX, false)
    if obs.size() > 0 and obEnabled and timeframeAllowed and showHistoricZones
        for i = 0 to obs.size() - 1
            ObRec o = obs.get(i)
            if not o.active
                if o.isBull
                    bullInvalid.push(i)
                else
                    bearInvalid.push(i)

if bearInvalid.size() > 0 and (not hideInvalid or breakerEnabled) and showHistoricZones and obEnabled and timeframeAllowed
    for i = 0 to bearInvalid.size() - 1
        ObRec ob = obs.get(bearInvalid.get(i))
        drawZoneAndVolume(ob)

if bearActive.size() > 0 and obEnabled and timeframeAllowed
    for i = 0 to bearActive.size() - 1
        ObRec ob = obs.get(bearActive.get(i))
        drawZoneAndVolume(ob)

if bullInvalid.size() > 0 and (not hideInvalid or breakerEnabled) and showHistoricZones and obEnabled and timeframeAllowed
    for i = 0 to bullInvalid.size() - 1
        ObRec ob = obs.get(bullInvalid.get(i))
        drawZoneAndVolume(ob)

if bullActive.size() > 0 and obEnabled and timeframeAllowed
    for i = 0 to bullActive.size() - 1
        ObRec ob = obs.get(bullActive.get(i))
        drawZoneAndVolume(ob)

// Draw all stored retest events on top of zones
if obEnabled and timeframeAllowed
    drawRetestLabels(bullActive, bearActive, bullInvalid, bearInvalid)
//#endregion

//#region ALERTS
alertcondition(alBullOB and evNewBullOB, "Bullish Order Block", "New Bullish Order Block detected.")
alertcondition(alBearOB and evNewBearOB, "Bearish Order Block", "Bearish Order Block detected.")
alertcondition(alBullRetest and evBullRetest, "Bullish OB Retest", "Bullish Order Block retest.")
alertcondition(alBearRetest and evBearRetest, "Bearish OB Retest", "Bearish Order Block retest.")
alertcondition(bullConfluence, "Bullish OB + FVG + BOS", "Bullish order block, FVG, and BOS confluence.")
alertcondition(bearConfluence, "Bearish OB + FVG + BOS", "Bearish order block, FVG, and BOS confluence.")

if alBullOB and evNewBullOB
    alert("Bullish Order Block detected.", alert.freq_once_per_bar)

if alBearOB and evNewBearOB
    alert("Bearish Order Block detected.", alert.freq_once_per_bar)

if alBullRetest and evBullRetest
    alert("Bullish Order Block retest.", alert.freq_once_per_bar)

if alBearRetest and evBearRetest
    alert("Bearish Order Block retest.", alert.freq_once_per_bar)

if bullConfluence
    alert("Bullish OB + FVG + BOS confluence.", alert.freq_once_per_bar)

if bearConfluence
    alert("Bearish OB + FVG + BOS confluence.", alert.freq_once_per_bar)
//#endregion
